Skip to content

feat: add profile-based routing foundation#161

Closed
davetha wants to merge 4 commits intorynfar:mainfrom
davetha:feature/profile-routing-foundation
Closed

feat: add profile-based routing foundation#161
davetha wants to merge 4 commits intorynfar:mainfrom
davetha:feature/profile-routing-foundation

Conversation

@davetha
Copy link
Copy Markdown
Contributor

@davetha davetha commented Mar 26, 2026

Summary

  • add a profile-based routing foundation so Meridian can distinguish between multiple Claude contexts instead of assuming one global account/session space
  • scope session lookup and resume by profile, and allow profile selection via x-meridian-profile
  • add a local example for running one Meridian instance with personal/company profiles on the same port

Use Case

I have both a personal and a company Claude account/plan, and I want to use OpenCode from several systems without having to install and auth Meridian separately on each machine.

The goal is to run a single Meridian instance that I can reach over my local network or a private Tailscale tunnel, then route requests to the right Claude profile from each client. That gives me better control over which systems use which account, while still feeling similar to how each machine might otherwise have its own claude auth setup.

This PR only adds the profile-routing foundation. It does not add request authentication or admin-route protection yet.

Testing

  • bun test src/__tests__/proxy-profiles.test.ts
  • bun test src/__tests__/proxy-stale-uuid-retry.test.ts
  • npm run build

Notes

Follow-up PRs will keep the maintainer review small:

  1. config file loading + optional request API keys
  2. optional protection for /health and /telemetry*

@rynfar
Copy link
Copy Markdown
Owner

rynfar commented Mar 27, 2026

I'll review this in the morning! Thank you for. the submission and ideas!

Copy link
Copy Markdown
Owner

@rynfar rynfar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good work on this — the scope is right, the session isolation approach is clean, and the per-profile auth caching is well done. I want to merge this once two things are adjusted:

1. Move getProfileId out of AgentAdapter

x-meridian-profile is Meridian proxy infrastructure, not an agent-specific concern. Every adapter would implement it identically — it's just c.req.header("x-meridian-profile"). The AgentAdapter interface should stay focused on abstracting agent differences (OpenCode vs. future agents).

Read the header directly in server.ts instead:

const requestedProfileId = c.req.header("x-meridian-profile")
const requestedProfile = resolveProfile(finalConfig, requestedProfileId)

And drop getProfileId from adapter.ts and adapters/opencode.ts.

2. Handle unknown profile as a 400, not a 500

Right now if a client sends x-meridian-profile: nonexistent, resolveProfile() throws, which hits the generic catch in server.ts and returns a 500 with a stack trace. This should be a clean 400.

Easiest fix — catch it at the call site in server.ts:

let requestedProfile: ResolvedProfile
try {
  requestedProfile = resolveProfile(finalConfig, requestedProfileId)
} catch (e) {
  return c.json({
    type: "error",
    error: { type: "invalid_request_error", message: e instanceof Error ? e.message : "Invalid profile" },
  }, 400)
}

Everything else looks good. The buildScopedKey approach, the per-profile auth status cache, and the test coverage are all solid. Happy to merge once these two are in.

@davetha
Copy link
Copy Markdown
Contributor Author

davetha commented Mar 30, 2026

Thanks for the feedback. Whenever I get some free time I'll get the suggestions added in, and continue testing.

@rynfar
Copy link
Copy Markdown
Owner

rynfar commented Apr 5, 2026

This is a genuinely useful feature and I spent some time redesigning this to make it more meaningful. I've added support for multiple profiles. There is also a UI to help you manage your profiles with clear instructions in how to authenticate each profile and how to switch bertween them easily - there is also a cli for this if you prefer the CLI.

Right now you can only have 1 profile active at a time, i may extend this to allow 2 profiles under different ports running at the same time depending on if this is a mode people want.

Right now you will be able to create propfiles and simply swap between them at the proxy level, this will allow you to swap without losing resumability for a session you currentlyl have active.

Give it a spin it will be in the next release. I am doing some testing and then i will release it.

@davetha

@davetha
Copy link
Copy Markdown
Contributor Author

davetha commented Apr 5, 2026

I haven't had time, but I'll give it a whirl.

One thing I should mention is the refresh token wasn't working on the secondary Claude/non-default path.

Let me know if you run into the same issue. It may have been a docker issue or something I had misconfigured.

@rynfar
Copy link
Copy Markdown
Owner

rynfar commented Apr 5, 2026

I haven't had time, but I'll give it a whirl.

One thing I should mention is the refresh token wasn't working on the secondary Claude/non-default path.

Let me know if you run into the same issue. It may have been a docker issue or something I had misconfigured.

Yea I noticed that while i was testing and am fixing this as part of it. I should have a release for this later today once I've had enough time to stress test it a bit.

The UI will keep track of the last successful refresh for each profile and will refresh both profiles.

You still have to manually log out of oauth before adding a second account. This part is janky and really hard to code around so its not really worth it. For now you just have to manage the login yourself. But once logged in the proxy will keep your profiles refreshed. I put clear instructions in the UI so its easy to follow.

rynfar added a commit that referenced this pull request Apr 5, 2026
Run one Meridian instance with multiple Claude accounts. Each profile
is a named auth context with its own CLAUDE_CONFIG_DIR and OAuth tokens.
Unlimited profiles supported.

CLI commands:
  meridian profile add <name>       # authenticate and add a profile
  meridian profile list             # show profiles and auth status
  meridian profile switch <name>    # switch active profile on running proxy
  meridian profile remove <name>    # remove a profile
  meridian profile login <name>     # re-authenticate a profile

Profile selection priority:
  1. x-meridian-profile request header (per-request override)
  2. Active profile (set via UI dropdown, CLI, or POST /profiles/active)
  3. Default profile from config
  4. First configured profile

UI:
- Global sticky profile bar on all pages (landing + telemetry)
- Profile dropdown with instant switching
- Nav links (Home / Telemetry)
- Auto-hides when no profiles configured

API:
- GET /profiles — list profiles with active indicator
- POST /profiles/active — switch the active profile

Internals:
- Profiles stored in ~/.config/meridian/profiles.json
- Per-profile auth dirs in ~/.config/meridian/profiles/{id}/
- Auto-loads from profiles.json when MERIDIAN_PROFILES env not set
- Per-profile auth status caching (keyed by env overrides)
- Session resume scoped by profile ID
- Background auth keepalive for all profiles (45s interval)
- Zero impact when no profiles configured

Addresses #161
@rynfar rynfar closed this in #279 Apr 5, 2026
rynfar added a commit that referenced this pull request Apr 5, 2026
…ng (#279)

* feat: multi-profile support — switch Claude accounts without restarting

Run one Meridian instance with multiple Claude accounts. Each profile
is a named auth context with its own CLAUDE_CONFIG_DIR and OAuth tokens.
Unlimited profiles supported.

CLI commands:
  meridian profile add <name>       # authenticate and add a profile
  meridian profile list             # show profiles and auth status
  meridian profile switch <name>    # switch active profile on running proxy
  meridian profile remove <name>    # remove a profile
  meridian profile login <name>     # re-authenticate a profile

Profile selection priority:
  1. x-meridian-profile request header (per-request override)
  2. Active profile (set via UI dropdown, CLI, or POST /profiles/active)
  3. Default profile from config
  4. First configured profile

UI:
- Global sticky profile bar on all pages (landing + telemetry)
- Profile dropdown with instant switching
- Nav links (Home / Telemetry)
- Auto-hides when no profiles configured

API:
- GET /profiles — list profiles with active indicator
- POST /profiles/active — switch the active profile

Internals:
- Profiles stored in ~/.config/meridian/profiles.json
- Per-profile auth dirs in ~/.config/meridian/profiles/{id}/
- Auto-loads from profiles.json when MERIDIAN_PROFILES env not set
- Per-profile auth status caching (keyed by env overrides)
- Session resume scoped by profile ID
- Background auth keepalive for all profiles (45s interval)
- Zero impact when no profiles configured

Addresses #161

* fix: address code review findings for multi-profile feature

P0: Set 0o600 permissions on profiles.json (API keys not world-readable)
P1: Replace empty catch blocks with console.warn logging
P1: Add 21 unit tests for profiles.ts pure functions
P2: Import ProfileConfig from profiles.ts (eliminate 3 duplicate types)
P2: Use profile ID as auth cache key instead of JSON.stringify(envOverrides)
P2: Cache loadProfilesFromDisk() with 5s TTL (avoid sync I/O per request)
P2: HTML-escape profile IDs in profilePage.ts and profileBar.ts (XSS fix)
P2: Add try/catch for JSON parsing in POST /profiles/active
P2: Add CLAUDE_PROXY_PORT/HOST fallback in profileSwitch()
P2: Update ARCHITECTURE.md module map and dependency rules
P2: Update CLAUDE.md stable API contract with new profile endpoints/headers
P3: Add multi-profile documentation to README with examples
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants